Registry 与镜像管理
镜像构建出来之后总要有个地方存放和分发,这个地方就是 Registry(镜像仓库)。本篇前半部分讲 Registry 的原理和常见服务(Docker Hub、Harbor、各大云厂商的仓库),后半部分讲日常运维会用到的事:镜像同步、拉取加速、签名、漏洞扫描和过期镜像清理。
Registry 原理
镜像命名规则
一个完整的镜像地址长这样:
registry.example.com/命名空间/仓库名:标签
以 registry.cn-hangzhou.aliyuncs.com/myteam/nginx:1.27 为例,拆开看:
| 部分 | 示例 | 说明 |
|---|---|---|
| Registry 地址 | registry.cn-hangzhou.aliyuncs.com | 仓库服务器的域名,省略时默认是 Docker Hub(docker.io) |
| 命名空间 | myteam | 也叫项目名或组织名,用来分组和控权限 |
| 仓库名 | nginx | 一个镜像仓库,存同一个应用的各个版本 |
| 标签 | 1.27 | 区分版本,省略时默认 latest |
几个容易搞混的点:
nginx:1.27是docker.io/library/nginx:1.27的简写,官方镜像的命名空间固定是library,可以省略。myuser/nginx:1.27实际指向docker.io/myuser/nginx:1.27,不是本地起的名字。- 标签只是指针,同一个镜像可以同时打多个标签;镜像真正的身份是 digest(内容摘要),形如
sha256:xxxxx。
push / pull 时分层传输
镜像是由一层一层的只读层叠起来的(见前面《核心概念与架构》一篇)。Registry 存的就是这些层加一份 manifest(清单,记录镜像由哪些层组成、每层 digest 是什么)。
push 的时候:
- 客户端先问 Registry「这些层你有没有」(按 digest 查询)。
- 已有的层直接跳过,只上传缺失的层。
- 层传完后上传镜像配置和 manifest,打上标签。
pull 的时候反过来:先拿 manifest,再逐层下载本地没有的层,最后组装成镜像。所以两台机器拉同一个镜像,第二次往往快很多——大部分层本地已经有了。
这也是「镜像不要做得太大、公共部分尽量放基础镜像里」的原因之一:层可以跨镜像复用。同一个仓库里十个应用都基于 ubuntu:24.04 构建,这个基础层在 Registry 上只存一份。
Docker Hub
Docker Hub(hub.docker.com)是 Docker 官方仓库,也是最常用的公共 Registry。
官方仓库与命名空间
- 不带前缀的镜像名(
nginx、redis、mysql)是 Docker 官方维护的 Official Image,质量有保障,优先用。 - 带
用户名/或组织名/前缀的是个人或组织发布的,用之前看一眼下载量和更新频率。 dockercoins这类 Verified Publisher 徽章表示发布者经过官方认证,比纯匿名账号靠谱。
push 到自己的命名空间:
# 登录,按提示输入用户名和密码(或 Access Token)
docker login
# 打上带自己用户名的标签再推
docker tag myapp:1.0 kuizuo/myapp:1.0
docker push kuizuo/myapp:1.0
Docker Hub 从 2023 年起收紧了安全策略,建议用 Access Token 代替密码登录(网页端 Security 页面生成)。另外免费账号的私有仓库数量有限,私有镜像多的话考虑 Harbor 或云厂商仓库。
拉取限速
匿名拉取 Docker Hub 镜像是限速的,按 IP 计数,超出后报 429 Too Many Requests。大致的额度:
| 用户类型 | 拉取额度 |
|---|---|
| 匿名用户 | 每 6 小时 100 次 |
| 登录的免费账号 | 每 6 小时 200 次 |
| 付费账号 | 不限 |
实际踩坑场景:公司内网几十台机器走同一个出口 IP 拉镜像,很容易就撞到匿名限额。解决办法有三个:
docker login后再拉,按账号计数而不是按 IP。- 配置镜像加速器,见后文「镜像加速」一节。
- 把常用镜像同步到内部 Harbor,一劳永逸。
Harbor
Harbor 是 CNCF 毕业的开源私有 Registry,企业自建仓库的首选。除了存镜像,还提供项目管理、用户权限、镜像复制、漏洞扫描等一整套功能。
用 docker compose 部署
官方提供 offline installer(离线安装包),里面打包了所有需要的镜像,机器不能上外网也能装。
# 下载 offline installer(版本号换成当时的最新版)
wget https://github.com/goharbor/harbor/releases/download/v2.11.0/harbor-offline-installer-v2.11.0.tgz
# 解压并进入目录
tar xzvf harbor-offline-installer-v2.11.0.tgz
cd harbor
# 复制配置模板
cp harbor.yml.tmpl harbor.yml
编辑 harbor.yml,至少要改这几项:
hostname: harbor.example.com # 本机域名或 IP
http:
port: 80
# 管理员初始密码,登录后记得改
harbor_admin_password: Harbor12345
# 数据存放目录
data_volume: /data
然后执行安装脚本(它会生成 compose 文件并启动):
# 不想装扫描器等组件可以加 --with-notary 等参数控制
./install.sh
装完用 docker compose ps 确认各容器状态,浏览器访问 https://harbor.example.com,用 admin 加配置的密码登录。
要启用 HTTPS 需要提前备好证书,在 harbor.yml 里配置 https 段的证书路径。内网自用可以只用 HTTP,但客户端的 Docker 要把这个地址加进 insecure-registries,否则默认拒绝走 HTTP。
核心功能
- 项目(Project):镜像的分组单位,对应命名空间。每个项目可设公开或私有,推送地址形如
harbor.example.com/项目名/镜像名:标签。 - 用户与角色:支持本地用户,也能接 LDAP / OIDC。项目内分项目管理员、开发者、访客等角色,控制谁能推、谁只能拉。
- 复制(Replication):在两个 Harbor(或 Harbor 与其他 Registry)之间按规则同步镜像,常用于多地部署和灾备。
- 扫描(Scanner):内置 Trivy 作为默认扫描器,推送后自动或手动扫漏洞,还能配置「高危漏洞镜像禁止拉取」。
- 保留策略(Retention):按规则自动清理旧镜像,详见最后一节。
日常推送:
docker login harbor.example.com
docker tag myapp:1.0 harbor.example.com/myteam/myapp:1.0
docker push harbor.example.com/myteam/myapp:1.0
GHCR
GHCR(GitHub Container Registry,ghcr.io)是 GitHub 的镜像仓库,和 GitHub 仓库、Actions 集成得很顺,开源项目用得很多。
登录用的是 GitHub 的 Personal Access Token(PAT),需要勾选 read:packages 和 write:packages 权限:
# 把 PAT 通过管道传给 login,避免出现在 shell 历史里
echo $GITHUB_TOKEN | docker login ghcr.io -u 你的用户名 --password-stdin
镜像地址格式:ghcr.io/用户名或组织名/镜像名:标签。
docker tag myapp:1.0 ghcr.io/kuizuo/myapp:1.0
docker push ghcr.io/kuizuo/myapp:1.0
在 GitHub Actions 里推送时不用自己造 PAT,直接用内置的 GITHUB_TOKEN(secrets.GITHUB_TOKEN)登录即可,记得给 workflow 加上 packages: write 权限。
阿里云 ACR
阿里云容器镜像服务 ACR 分个人版(免费)和企业版。个人版地址格式:
registry.cn-<地域>.aliyuncs.com/<命名空间>/<仓库名>:<标签>
例如 registry.cn-hangzhou.aliyuncs.com/myteam/myapp:1.0。命名空间和仓库名都要先在控制台创建(仓库也可以推送时自动创建,取决于设置)。
登录密码不是阿里云账号密码,而是在控制台「访问凭证」里单独设置的 Registry 登录密码:
docker login --username=阿里云账号 registry.cn-hangzhou.aliyuncs.com
docker tag myapp:1.0 registry.cn-hangzhou.aliyuncs.com/myteam/myapp:1.0
docker push registry.cn-hangzhou.aliyuncs.com/myteam/myapp:1.0
个人版实例只有部分地域可用,选离自己服务器近的地域。ECS 内网拉取可以把域名换成 registry-vpc.cn-<地域>.aliyuncs.com,走内网不占公网带宽。
腾讯云 TCR
腾讯云容器镜像服务 TCR 主推企业版实例,每个实例有独立域名。地址格式:
<实例名>.tencentcloudcr.com/<命名空间>/<仓库名>:<标签>
另外还有老的个人版 CCR,地址是 ccr.ccs.tencentyun.com/<命名空间>/<仓库名>:<标签>,新用户建议直接用 TCR。
登录(密码在 TCR 控制台的「访问凭证」里获取):
docker login myinstance.tencentcloudcr.com --username=用户名
docker tag myapp:1.0 myinstance.tencentcloudcr.com/myteam/myapp:1.0
docker push myinstance.tencentcloudcr.com/myteam/myapp:1.0
AWS ECR
AWS 的 Elastic Container Registry,地址格式里带账号 ID 和区域:
<账号ID>.dkr.ecr.<区域>.amazonaws.com/<仓库名>:<标签>
ECR 的登录密码是临时的(12 小时过期),要用 AWS CLI 现取:
# get-login-password 生成临时密码,管道直接交给 docker login
aws ecr get-login-password --region ap-northeast-1 \
| docker login --username AWS --password-stdin 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com
# 仓库需要先创建(已存在会报错,可忽略)
aws ecr create-repository --repository-name myapp --region ap-northeast-1
docker tag myapp:1.0 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/myapp:1.0
docker push 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/myapp:1.0
Google Artifact Registry
Google Cloud 的 Artifact Registry(GAR)替代了旧的 Container Registry。地址格式:
<区域>-docker.pkg.dev/<项目ID>/<仓库名>/<镜像名>:<标签>
比别家多一层「仓库名」。认证方式一般用 gcloud 配置成 Docker 的凭据助手,一次配置长期有效:
# 配置凭据助手,之后 docker 命令自动带认证
gcloud auth configure-docker asia-northeast1-docker.pkg.dev
docker tag myapp:1.0 asia-northeast1-docker.pkg.dev/my-project/my-repo/myapp:1.0
docker push asia-northeast1-docker.pkg.dev/my-project/my-repo/myapp:1.0
CI 环境里不建议用 gcloud,改用服务账号密钥:把 JSON 密钥文件内容通过 docker login -u _json_key --password-stdin 传入即可。
Registry API
Registry 提供一套标准的 HTTP API(OCI Distribution Spec),不装 Docker 也能用 curl 直接操作仓库。所有接口都在 /v2/ 路径下。
先准备认证。以 basic auth(Harbor、自建 Registry 常见)为例:
# 变量方便后面复用
REGISTRY=harbor.example.com
AUTH="admin:Harbor12345"
查看仓库里都有哪些镜像仓库:
curl -u "$AUTH" https://$REGISTRY/v2/_catalog
{"repositories":["myteam/myapp","myteam/nginx"]}
查看某个仓库的所有标签:
curl -u "$AUTH" https://$REGISTRY/v2/myteam/myapp/tags/list
{"name":"myteam/myapp","tags":["1.0","1.1","latest"]}
查询某个标签的 manifest(拿 digest 就靠它):
curl -u "$AUTH" \
-H "Accept: application/vnd.docker.distribution.manifest.v2+json" \
https://$REGISTRY/v2/myteam/myapp/manifests/1.0
删除镜像要先拿到 digest,再按 digest 删。获取 digest 用 HEAD 请求读响应头:
# 拿 digest(响应头里的 Docker-Content-Digest)
DIGEST=$(curl -sI -u "$AUTH" \
-H "Accept: application/vnd.docker.distribution.manifest.v2+json" \
https://$REGISTRY/v2/myteam/myapp/manifests/1.0 \
| grep -i docker-content-digest | awk '{print $2}' | tr -d '\r')
# 按 digest 删除
curl -u "$AUTH" -X DELETE https://$REGISTRY/v2/myteam/myapp/manifests/$DIGEST
两个坑:一是删除接口默认可能是关的,Registry(含 Harbor 底层)需要开启 delete 配置;二是删除只是删了 manifest 引用,层文件还在磁盘上,要跑垃圾回收(Harbor 在「垃圾清理」里手动触发或定时执行)才真正释放空间。
另外注意 Docker Hub 的认证方式不同,要先去 auth.docker.io 换 token 再请求 registry-1.docker.io,不能直接 basic auth。
镜像同步
换 Registry 的时候(比如从 Docker Hub 迁到内部 Harbor,或同步到国外拉不动的镜像),需要把镜像从 A 仓库搬到 B 仓库。
手动三板斧:pull / tag / push
适合偶尔同步一两个镜像:
# 1. 从源仓库拉下来
docker pull nginx:1.27-alpine
# 2. 打上目标仓库的标签
docker tag nginx:1.27-alpine harbor.example.com/library/nginx:1.27-alpine
# 3. 推到目标仓库
docker push harbor.example.com/library/nginx:1.27-alpine
缺点很明显:要先下载到本地再上传,慢;而且默认只搬当前架构,多架构镜像会丢其他平台的层。
用 skopeo 批量同步
skopeo 是专门干镜像搬运的工具,优点是不用落地本地 Docker,直接在两个 Registry 之间流式传输,还能保留多架构 manifest:
# 单个镜像同步(--all 保留所有架构)
skopeo copy --all \
docker://docker.io/library/nginx:1.27-alpine \
docker://harbor.example.com/library/nginx:1.27-alpine \
--src-creds 用户名:密码 \
--dest-creds admin:Harbor12345
批量同步整个仓库的所有标签:
skopeo sync --src docker --dest docker \
--src-creds 用户名:密码 \
--dest-creds admin:Harbor12345 \
docker.io/library/nginx harbor.example.com/library
skopeo 也支持同步到本地目录(dir:)、OCI 归档(oci:)等中间格式,内网完全隔离的环境可以先导出成文件再拷进去。
如果只是临时拉一个镜像(比如 CI 里),也可以不同步整个仓库,直接 docker pull harbor.example.com/mirror/nginx:1.27 用同步好的内部地址,业务代码里的镜像名统一切过去。
镜像加速
国内直连 Docker Hub 慢甚至拉不动,常用两种解决办法。
方式一:配置 registry-mirrors
编辑 /etc/docker/daemon.json(不存在就新建),加上 registry-mirrors:
{
"registry-mirrors": [
"https://docker.1ms.run",
"https://docker.xuanyuan.me"
]
}
改完重启 Docker 生效:
sudo systemctl daemon-reload
sudo systemctl restart docker
# 验证配置是否生效
docker info | grep -A 5 "Registry Mirrors"
配置后 docker pull nginx 会自动走镜像站。优点是透明,不用改镜像名;缺点是镜像站质量参差,有些会失效或限速,需要自己挑能用的。
registry-mirrors 只对 Docker Hub 生效,拉 ghcr.io、gcr.io 等其他仓库不会走镜像站。这类仓库要么用下面的前缀代理,要么直接同步到内部仓库。
方式二:用代理仓库前缀
一些公共加速服务支持「加前缀」的方式拉取,相当于把镜像站当成代理,明确指定走它:
# 把 docker.io 换成代理域名
docker pull docker.1ms.run/library/nginx:1.27-alpine
# ghcr.io 的镜像有些代理也支持
docker pull ghcr.1ms.run/owner/repo:tag
拉下来之后镜像名里带着前缀,为了和部署配置一致,一般要重新打标签:
docker tag docker.1ms.run/library/nginx:1.27-alpine nginx:1.27-alpine
这种方式不用改 daemon 配置、不用重启,临时用一次很方便。
镜像签名(Cosign)
镜像是分层文件加一份 manifest,传过来的东西有没有被篡改、是不是你信任的人发布的,靠签名来确认。Cosign 是 sigstore 项目的签名工具,目前容器镜像签名的事实标准。
本地密钥对签名
适合小团队自用,生成一对密钥,私钥签名、公钥验证:
# 生成密钥对(会提示设置私钥密码)
cosign generate-key-pair
# 生成 cosign.key(私钥)和 cosign.pub(公钥)
# 用私钥给镜像签名(镜像必须在 Registry 上,先 push 再签)
cosign sign --key cosign.key harbor.example.com/myteam/myapp:1.0
# 别人拿到公钥后验证
cosign verify --key cosign.pub harbor.example.com/myteam/myapp:1.0
验证通过会输出签名信息;镜像被改过(digest 对不上)就验证失败。
Keyless 签名
Keyless 模式不管理密钥,用 OIDC 身份(GitHub、Google 账号等)换临时证书签名,签名记录上公共透明日志 Rekor,适合开源项目和 CI:
# 会弹出浏览器做 OIDC 认证(CI 里用 --identity-token 免交互)
cosign sign harbor.example.com/myteam/myapp:1.0
# 验证时指定签名者身份和签发者
cosign verify harbor.example.com/myteam/myapp:1.0 \
--certificate-identity your-email@example.com \
--certificate-oidc-issuer https://accounts.google.com
签名本身也是作为一个特殊的 OCI 对象存在 Registry 里(标签形如 sha256-xxxx.sig),所以 Registry 得支持 OCI artifact——Harbor 2.x、GHCR、ECR、GAR 都支持。私钥一定保管好,丢了等于签名作废,泄露了别人能冒充你签。
镜像扫描(Trivy)
镜像里装了什么包、有没有已知 CVE 漏洞,靠扫描来查。Trivy 是 Aqua 开源的一体化扫描器,扫镜像、扫文件系统、扫配置都行,Harbor 默认集成的就是它。
扫描镜像:
# 扫描本地或远程镜像
trivy image nginx:1.27-alpine
# 只看高危和严重漏洞(CI 里常用)
trivy image --severity HIGH,CRITICAL nginx:1.27-alpine
# 发现高危漏洞就以非零码退出,方便 CI 卡点
trivy image --exit-code 1 --severity CRITICAL myapp:1.0
扫描文件系统(构建前扫源代码依赖):
# 扫当前目录的依赖清单(package-lock.json、go.sum 等)
trivy fs .
# 连 Dockerfile 错误配置一起扫
trivy config .
CI 里怎么用
以 GitHub Actions 为例,构建完镜像立刻扫,扫出严重漏洞就失败:
- name: Build image
run: docker build -t myapp:${{ github.sha }} .
- name: Scan image
uses: aquasecurity/trivy-action@master
with:
image-ref: myapp:${{ github.sha }}
severity: 'CRITICAL,HIGH'
exit-code: '1'
扫描结果依赖漏洞库,Trivy 每次运行会自动更新库。内网环境可以提前把漏洞库同步到内部,用 --db-repository 指定内网镜像地址。别扫一次就完事——镜像没重build,新爆出的 CVE 也会影响它,要定期重扫仓库里的存量镜像(Harbor 可以定时全量扫)。
SBOM
SBOM(Software Bill of Materials,软件物料清单)就是一张「这个软件里装了哪些组件、各是什么版本」的清单。有了它,出漏洞时(比如 Log4Shell 那种)能马上查出来哪些镜像受影响,不用挨个翻镜像。
生成 SBOM 常用两个工具:
syft,Anchore 出的专用 SBOM 工具:
# 扫描镜像,输出 SPDX 格式的 SBOM
syft myapp:1.0 -o spdx-json > sbom.spdx.json
# 也可以扫目录(构建产物、源码)
syft ./dist -o cyclonedx-json > sbom.cdx.json
Trivy 也能顺手生成,不用多装一个工具:
# 把镜像扫描结果输出为 SBOM
trivy image --format cyclonedx -o sbom.json myapp:1.0
# 扫描文件系统生成
trivy fs --format spdx-json -o sbom.spdx.json .
主流格式就两个:SPDX 和 CycloneDX,选哪个都行,关键是每次构建都生成、和镜像一起存起来(可以直接 cosign attach sbom 挂到镜像上,或存到制品库)。出了新漏洞拿 SBOM 一搜就知道影响面。
镜像生命周期管理
仓库里的镜像只增不减,迟早把磁盘撑爆。镜像生命周期管理就是回答两个问题:哪些镜像该留、留多久,到期的怎么清掉。
保留策略的思路
常见规则组合着用:
- 按数量:每个仓库只保留最近 N 个标签,比如保留最近 10 个,CI 每次发版都推新标签,老的自动淘汰。
- 按时间:保留最近 N 天推送或拉取过的镜像,超过没人动的清掉。
- 按标签模式:
release-*和latest永远保留,*-dev、*-test这类临时标签只留 3 天。 - 按拉取情况:「最近 90 天被拉取过」的留着——长期没人拉的镜像多半已经废弃。
Harbor 的保留规则
Harbor 在项目级别配置保留规则(Retention Policy),支持组合条件:
- 进入项目 → 「策略」→ 添加规则。
- 选匹配范围:哪些仓库、哪些标签(支持
release-*这类通配,也能排除)。 - 选保留条件:最近 N 个 / 最近 N 天推送的 / 最近 N 天拉取的。
- 设置执行周期(每小时 / 每天 / 每周)或手动跑。
规则跑完后被删的标签变成「无标签」的悬空引用,还要再跑一次垃圾清理(Garbage Collection)才真正回收磁盘空间。Harbor 的 GC 可以在系统管理里设成定时任务。
手动清理的思路
没有 Harbor 这种工具时,用 Registry API 写个脚本也能干:
GET /v2/_catalog列出所有仓库。- 逐个
GET /v2/<仓库>/tags/list拿标签。 - 对每个标签查 manifest 里的创建时间,挑出要删的。
- 按 digest 调 DELETE 接口删除。
- 在 Registry 服务器上跑垃圾回收,例如 Distribution 官方 Registry 的
registry garbage-collect /etc/docker/registry/config.yml。
删除策略上线前先用「试运行」验证(Harbor 的规则有 Dry Run 按钮),确认规则匹配的是你想删的那批再放开。误删正在用的镜像,下一次拉取或扩容直接就挂了。另外删除前确认这些镜像能重新构建出来——Dockerfile 和构建环境还在,删了才不可怕。